package org.tulkas.webclient.html;

import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import nu.validator.htmlparser.common.XmlViolationPolicy;
import nu.validator.htmlparser.dom.DefaultTreeBuilder;
import nu.validator.htmlparser.dom.Dom2Sax;
import nu.validator.htmlparser.impl.Tokenizer;
import nu.validator.htmlparser.impl.TreeBuilderProxy;
import nu.validator.htmlparser.io.Driver;
import nu.validator.htmlparser.sax.HtmlSerializer;

import org.tulkas.webclient.exception.ParseException;
import org.tulkas.webclient.select.Selector;
import org.tulkas.webclient.util.DomUtil;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class HtmlFragment {
	private static final int DEFAULT_PTN_PARAMS = Pattern.CASE_INSENSITIVE
			| Pattern.DOTALL;
	private static final XmlViolationPolicy DEFAULT_XML_POLICY = XmlViolationPolicy.ALLOW;

	private volatile String text;

	private volatile Node domNode;

	public HtmlFragment(String text) {
		if (text == null) {
			throw new NullPointerException("text can't be null");
		}
		this.text = text;
	}

	public HtmlFragment(Node node) {
		if (node == null) {
			throw new NullPointerException("node can't be null");
		}
		this.domNode = node;
	}

	public String getText() {
		if (text == null) {
			domToText();
		}
		return text;
	}

	public Element getElement() {
		Node node = getNode();
		if (node instanceof Document) {
			return ((Document) node).getDocumentElement();
		} else if (node instanceof DocumentFragment) {
			return DomUtil.getFirstSubElement(node);
		} else {
			return (Element) node;
		}
	}

	public Node getNode() {
		if (domNode == null) {
			textToDom();
		}
		return domNode;
	}

	@Override
	public String toString() {
		return getText();
	}

	private Pattern buildPattern(String regexp) {
		return Pattern.compile(regexp, DEFAULT_PTN_PARAMS);
	}

	public HtmlFragment searchFirst(String regexp) {
		return searchFirst(buildPattern(regexp), 0);
	}

	public HtmlFragment searchFirst(String regexp, int groupIndex) {
		return searchFirst(buildPattern(regexp), groupIndex);
	}

	public HtmlFragment searchFirst(Pattern regexp) {
		return searchFirst(regexp, 0);
	}

	public HtmlFragment searchFirst(Pattern regexp, int groupIndex) {
		String text = getText();
		if (text == null) {
			return null;
		}
		Matcher mt = regexp.matcher(text);
		if (mt.find()) {
			return new HtmlFragment(mt.group(groupIndex));
		} else {
			return null;
		}
	}

	public HtmlFragment[] search(String regexp) {
		return search(buildPattern(regexp), 0);
	}

	public HtmlFragment[] search(String regexp, int groupIndex) {
		return search(buildPattern(regexp), groupIndex);
	}

	public HtmlFragment[] search(Pattern regexp) {
		return search(regexp, 0);
	}

	public HtmlFragment[] search(Pattern regexp, int groupIndex) {
		String text = getText();
		List<HtmlFragment> retList = new ArrayList<HtmlFragment>();
		if (text != null) {
			Matcher mt = regexp.matcher(text);
			while (mt.find()) {
				String value = mt.group(groupIndex);
				retList.add(new HtmlFragment(value));
			}
		}
		return retList.toArray(new HtmlFragment[retList.size()]);
	}

	public HtmlFragment[][] search(Pattern regexp, int[] groupIndexes) {
		List<HtmlFragment[]> retList = new ArrayList<HtmlFragment[]>();
		String text = getText();
		if (text != null) {
			Matcher mt = regexp.matcher(text);
			while (mt.find()) {
				HtmlFragment[] values = new HtmlFragment[groupIndexes.length];
				for (int i = 0; i < groupIndexes.length; i++) {
					values[i] = new HtmlFragment(mt.group(groupIndexes[i]));
				}
				retList.add(values);
			}
		}
		return retList.toArray(new HtmlFragment[retList.size()][]);
	}

	private void setParsingParameters(DefaultTreeBuilder treeBuilder,
			Driver driver) {
		driver.setNamePolicy(DEFAULT_XML_POLICY);
		treeBuilder.setNamePolicy(DEFAULT_XML_POLICY);
		treeBuilder.setIgnoringComments(true);
		driver.setXmlnsPolicy(DEFAULT_XML_POLICY);
		driver.setContentSpacePolicy(DEFAULT_XML_POLICY);
		driver.setContentNonXmlCharPolicy(DEFAULT_XML_POLICY);
		driver.setHtml4ModeCompatibleWithXhtml1Schemata(true);
	}

	protected synchronized void textToDom() {
		if (domNode != null || text == null) {
			return;
		}

		try {
			Reader reader = new StringReader(text);
			InputSource is = new InputSource(reader);
			DefaultTreeBuilder treeBuilder = new DefaultTreeBuilder();
			Tokenizer tk = new Tokenizer(new TreeBuilderProxy(treeBuilder),
					false);
			Driver driver = new Driver(tk);
			setParsingParameters(treeBuilder, driver);
			treeBuilder.setFragmentContext(null);
			driver.tokenize(is);
			Document doc = treeBuilder.getResultDoc();
			setupDomNode(doc, treeBuilder);
			reader.close();
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new ParseException("Error parsing html text to DOM node", e);
		}
	}

	private boolean containsContent(Node node) {
		if (node == null) {
			return false;
		}

		for (Node child = node.getFirstChild(); child != null; child = child
				.getNextSibling()) {
			if (child instanceof Comment) {
				continue;
			} else if (child instanceof Text) {
				Text tNode = (Text) child;
				String v = tNode.getNodeValue();
				if (v == null || "".equals(v.trim())) {
					continue;
				}
			}

			return true;
		}

		return false;
	}

	private DocumentFragment createDocFragmentForContent(Element parent) {
		Document doc = parent.getOwnerDocument();
		DocumentFragment ret = doc.createDocumentFragment();

		while (parent.hasChildNodes()) {
			ret.appendChild(parent.getFirstChild());
		}

		return ret;
	}

	private void setupDomNode(Document doc, DefaultTreeBuilder builder) {
		if (doc == null || doc.getDocumentElement() == null) {
			return;
		}

		Element rootEle = doc.getDocumentElement();
		Element head = DomUtil.getFirstSubElement(rootEle, "head");
		Element body = DomUtil.getFirstSubElement(rootEle, "body");

		if (builder.isDocDeclared()) {
			domNode = doc;
		} else {
			boolean hasHead = containsContent(head);
			boolean hasBody = containsContent(body);
			if (hasHead && hasBody) {
				domNode = doc;
			} else if (hasHead && !hasBody) {
				domNode = createDocFragmentForContent(head);
			} else if (!hasHead && hasBody) {
				domNode = createDocFragmentForContent(body);
			} else {
				domNode = doc.createDocumentFragment();
			}
		}
	}

	protected synchronized void domToText() {
		if (text != null || domNode == null) {
			return;
		}

		try {
			StringWriter writer = new StringWriter();
			HtmlSerializer se = new HtmlSerializer(writer);
			Dom2Sax dom2Sax = new Dom2Sax(se, se);
			dom2Sax.parse(domNode);
			text = writer.getBuffer().toString();
			writer.close();
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new ParseException("Error serializing DOM node to html text",
					e);
		}
	}

	public HtmlFragment[] select(String cssSelector) {
		return null;
	}

	public HtmlFragment selectFirst(String cssSelector) {
		return null;
	}

	public HtmlFragment[] select(Selector cssSelector) {
		return null;
	}

	public HtmlFragment selectFirst(Selector cssSelector) {
		return null;
	}

	public HtmlFragment[] evaluate(String xPathExpr)
			throws XPathExpressionException {
		XPath xpath = createXPath();
		return evaluate(xpath.compile(xPathExpr));
	}

	public HtmlFragment evaluateFirst(String xPathExpr)
			throws XPathExpressionException {
		HtmlFragment[] results = evaluate(xPathExpr);
		if (results != null && results.length > 0) {
			return results[0];
		} else {
			return null;
		}
	}

	public HtmlFragment[] evaluate(XPathExpression xPathExpr)
			throws XPathExpressionException {
		Node domNode = getNode();
		NodeList nodes = (NodeList) xPathExpr.evaluate(domNode,
				XPathConstants.NODESET);
		HtmlFragment[] ret = new HtmlFragment[nodes.getLength()];
		for (int i = 0; i < nodes.getLength(); i++) {
			ret[i] = new HtmlFragment(nodes.item(i));
		}
		return ret;
	}

	public HtmlFragment evaluateFirst(XPathExpression xPathExpr)
			throws XPathExpressionException {
		HtmlFragment[] results = evaluate(xPathExpr);
		if (results != null && results.length > 0) {
			return results[0];
		} else {
			return null;
		}
	}

	private XPath createXPath() {
		XPathFactory factory = XPathFactory.newInstance();
		return factory.newXPath();
	}

}
